package com.dny.asmtop;

import com.dny.asmtop.op.VarLocal;
import jdk.internal.org.objectweb.asm.Type;
import jdk.internal.org.objectweb.asm.commons.GeneratorAdapter;
import jdk.internal.org.objectweb.asm.commons.Method;

import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import static java.lang.ClassLoader.getSystemClassLoader;
import static java.lang.String.format;
import static java.util.Arrays.asList;
import static jdk.internal.org.objectweb.asm.Type.CHAR_TYPE;
import static jdk.internal.org.objectweb.asm.Type.getType;

/**
 * Created by jlutt on 2018-01-15.
 *
 * @author jlutt
 */
public final class ASMMethodUtils {

  private static final Map<String, Type> wrapperToPrimitive = new HashMap<>();

  static {
    for (Class<?> primitiveType : Primitives.allPrimitiveTypes()) {
      Class<?> wrappedType = Primitives.wrap(primitiveType);
      wrapperToPrimitive.put(wrappedType.getName(), getType(primitiveType));
    }
  }

  private static final Method BOOLEAN_VALUE = Method.getMethod("boolean booleanValue()");
  private static final Method CHAR_VALUE = Method.getMethod("char charValue()");
  private static final Method INT_VALUE = Method.getMethod("int intValue()");
  private static final Method FLOAT_VALUE = Method.getMethod("float floatValue()");
  private static final Method LONG_VALUE = Method.getMethod("long longValue()");
  private static final Method DOUBLE_VALUE = Method.getMethod("double doubleValue()");
  private static final Method SHORT_VALUE = Method.getMethod("short shortValue()");
  private static final Method BYTE_VALUE = Method.getMethod("byte byteValue()");

  private static final Type BOOLEAN_TYPE = getType(Boolean.class);
  private static final Type CHARACTER_TYPE = getType(Character.class);
  private static final Type BYTE_TYPE = getType(Byte.class);
  private static final Type SHORT_TYPE = getType(Short.class);
  private static final Type INT_TYPE = getType(Integer.class);
  private static final Type FLOAT_TYPE = getType(Float.class);
  private static final Type LONG_TYPE = getType(Long.class);
  private static final Type DOUBLE_TYPE = getType(Double.class);
  private static final Type VOID_TYPE = getType(Void.class);

  public static void generatorAndCast(MethodContext ctx, Command command, Type targetType) {
    Type type = command.generator(ctx);
    //生成返回值
    //等同mv.visitTypeInsn(CHECKCAST, type);
    checkCast(ctx, type, targetType);
  }

  public static void checkCast(MethodContext ctx, Type type, Type targetType) {
    GeneratorAdapter g = ctx.getGeneratorAdapter();

    if (type.equals(targetType)) {
      return;
    }

    if (targetType == Type.VOID_TYPE) {
      if (type.getSize() == 1) {
        g.pop();
      }
      if (type.getSize() == 2) {
        g.pop2();
      }
      return;
    }

    if (type == Type.VOID_TYPE) {
      throw new RuntimeException(String.format("Can't cast VOID_TYPE to %s. %s",
          targetType.getClassName(),
          exceptionInGeneratedClass(ctx)));
    }

    if (type.equals(ctx.getThisType())) {
      Class<?> javaType = getJavaType(ctx.getClassLoader(), targetType);
      if (javaType.isAssignableFrom(ctx.getMainClass())) {
        return;
      }
      for (Class<?> aClass : ctx.getOtherClasses()) {
        if (javaType.isAssignableFrom(aClass)) {
          return;
        }
      }
      throw new RuntimeException(format("Can't cast self %s to %s, %s",
          type.getClassName(),
          targetType.getClassName(),
          exceptionInGeneratedClass(ctx)));
    }

    if (!type.equals(ctx.getThisType()) && !targetType.equals(ctx.getThisType()) &&
        getJavaType(ctx.getClassLoader(), targetType).isAssignableFrom(getJavaType(ctx.getClassLoader(), type))) {
      return;
    }

    if (targetType.equals(getType(Object.class)) && isPrimitiveType(type)) {
      g.box(type);
      g.cast(wrap(type), getType(Object.class));
      return;
    }

    if ((isPrimitiveType(type) || isWrapperType(type)) &&
        (isPrimitiveType(targetType) || isWrapperType(targetType))) {

      Type targetTypePrimitive = isPrimitiveType(targetType) ? targetType : unwrap(targetType);

      if (isWrapperType(type)) {
        g.invokeVirtual(type, primitiveValueMethod(targetType));
        return;
      }

      assert isPrimitiveType(type);

      if (type != targetTypePrimitive) {
        g.cast(type, targetTypePrimitive);
      }

      if (isWrapperType(targetType)) {
        g.valueOf(targetTypePrimitive);
      }

      return;
    }

    g.checkCast(targetType);
  }

  public static String exceptionInGeneratedClass(MethodContext ctx) {
    return format("Throwed in generated class %s in method %s",
        ctx.getThisType().getClassName(),
        ctx.getMethod()
    );
  }

  public static boolean isPrimitiveType(Type type) {
    int sort = type.getSort();
    return sort == Type.BOOLEAN ||
        sort == Type.CHAR ||
        sort == Type.BYTE ||
        sort == Type.SHORT ||
        sort == Type.INT ||
        sort == Type.FLOAT ||
        sort == Type.LONG ||
        sort == Type.DOUBLE ||
        sort == Type.VOID;
  }

  public static boolean isWrapperType(Type type) {
    return type.getSort() == Type.OBJECT &&
        wrapperToPrimitive.containsKey(type.getClassName());
  }

  public static Method primitiveValueMethod(Type type) {
    if (type.equals(getType(char.class)) || type.equals(CHAR_TYPE))
      return CHAR_VALUE;
    if (type.equals(getType(boolean.class)) || type.equals(BOOLEAN_TYPE))
      return BOOLEAN_VALUE;
    if (type.equals(getType(double.class)) || type.equals(DOUBLE_TYPE))
      return DOUBLE_VALUE;
    if (type.equals(getType(float.class)) || type.equals(FLOAT_TYPE))
      return FLOAT_VALUE;
    if (type.equals(getType(long.class)) || type.equals(LONG_TYPE))
      return LONG_VALUE;
    if (type.equals(getType(int.class)) || type.equals(INT_TYPE))
      return INT_VALUE;
    if (type.equals(getType(short.class)) || type.equals(SHORT_TYPE))
      return SHORT_VALUE;
    if (type.equals(getType(byte.class)) || type.equals(BYTE_TYPE))
      return BYTE_VALUE;

    throw new RuntimeException(format("No primitive value method for %s ", type.getClassName()));
  }

  public static Class<?> getJavaType(ClassLoader classLoader, Type type) {
    int sort = type.getSort();
    if (sort == Type.BOOLEAN) {
      return boolean.class;
    }
    if (sort == Type.CHAR) {
      return char.class;
    }
    if (sort == Type.BYTE) {
      return byte.class;
    }
    if (sort == Type.SHORT) {
      return short.class;
    }
    if (sort == Type.INT) {
      return int.class;
    }
    if (sort == Type.FLOAT) {
      return float.class;
    }
    if (sort == Type.LONG) {
      return long.class;
    }
    if (sort == Type.DOUBLE) {
      return double.class;
    }
    if (sort == Type.VOID) {
      return void.class;
    }
    if (sort == Type.OBJECT) {
      try {
        return classLoader.loadClass(type.getClassName());
      } catch (ClassNotFoundException e) {
        throw new RuntimeException(format("No class %s in class loader", type.getClassName()), e);
      }
    }
    if (sort == Type.ARRAY) {
      Class<?> result;
      if (type.equals(getType(Object[].class))) {
        result = Object[].class;
      } else {
        String className = type.getDescriptor().replace('/', '.');
        try {
          result = Class.forName(className);
        } catch (ClassNotFoundException e) {
          throw new RuntimeException(format("No class %s in Class.forName", className), e);
        }
      }
      return result;
    }
    throw new RuntimeException(format("No Java type for %s", type.getClassName()));
  }

  public static Class<?> getJavaType(Type type) {
    return getJavaType(getSystemClassLoader(), type);
  }

  public static Class<?> tryGetJavaType(ClassLoader classLoader, Type type) {
    int sort = type.getSort();
    if (sort == Type.BOOLEAN) {
      return boolean.class;
    }
    if (sort == Type.CHAR) {
      return char.class;
    }
    if (sort == Type.BYTE) {
      return byte.class;
    }
    if (sort == Type.SHORT) {
      return short.class;
    }
    if (sort == Type.INT) {
      return int.class;
    }
    if (sort == Type.FLOAT) {
      return float.class;
    }
    if (sort == Type.LONG) {
      return long.class;
    }
    if (sort == Type.DOUBLE) {
      return double.class;
    }
    if (sort == Type.VOID) {
      return void.class;
    }
    if (sort == Type.OBJECT) {
      try {
        return classLoader.loadClass(type.getClassName());
      } catch (ClassNotFoundException e) {
        return Object.class;
      }
    }
    if (sort == Type.ARRAY) {
      Class<?> result;
      if (type.equals(getType(Object[].class))) {
        result = Object[].class;
      } else {
        String className = type.getDescriptor().replace('/', '.');
        try {
          result = Class.forName(className);
        } catch (ClassNotFoundException e) {
          throw new RuntimeException(format("No class %s in Class.forName", className), e);
        }
      }
      return result;
    }
    return Object.class;
  }

  public static Class<?> tryGetJavaType(Type type) {
    return tryGetJavaType(getSystemClassLoader(), type);
  }

  public static Type wrap(Type type) {
    int sort = type.getSort();
    if (sort == Type.BOOLEAN)
      return BOOLEAN_TYPE;
    if (sort == Type.CHAR)
      return CHARACTER_TYPE;
    if (sort == Type.BYTE)
      return BYTE_TYPE;
    if (sort == Type.SHORT)
      return SHORT_TYPE;
    if (sort == Type.INT)
      return INT_TYPE;
    if (sort == Type.FLOAT)
      return FLOAT_TYPE;
    if (sort == Type.LONG)
      return LONG_TYPE;
    if (sort == Type.DOUBLE)
      return DOUBLE_TYPE;
    if (sort == Type.VOID)
      return VOID_TYPE;

    throw new RuntimeException(format("%s is not primitive", type.getClassName()));
  }

  public static Type unwrap(Type type) {
    Preconditions.check(type.getSort() == Type.OBJECT);
    Type result = wrapperToPrimitive.get(type.getClassName());
    Preconditions.check(result != null);
    return result;
  }

  public static <T> String argsToString(T[] args) {
    return argsToString(asList(args));
  }

  public static <T> String argsToString(Collection<T> args) {
    return argsToString(args, ", ");
  }

  public static <T> String argsToString(Collection<T> args, String separator) {
    StringBuilder stringBuilder = new StringBuilder();
    Iterator<T> iterator = args.iterator();
    stringBuilder.append(iterator.next());
    while (iterator.hasNext()) {
      stringBuilder.append(separator).append(iterator.next());
    }
    return stringBuilder.toString();
  }

  public static void invokeVirtualOrInterface(GeneratorAdapter g, Class<?> owner, Method method) {
    if (owner.isInterface())
      g.invokeInterface(getType(owner), method);
    else
      g.invokeVirtual(getType(owner), method);
  }

  public static VarLocal newLocal(MethodContext ctx, Type type) {
    int local = ctx.getGeneratorAdapter().newLocal(type);
    return new VarLocal(local);
  }
}
